#include <GL/glew.h>
#include <GL/freeglut.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <vector>
#include <string>
#include <cstdio>
#include <ctime>
#include <common/common.hpp>
#include "particle_shader.hpp"
#include "particle_system.hpp"
#include "particle_shooter.hpp"
#include "skybox_shader.hpp"
#include "skybox.hpp"
#include "heightmap_shader.hpp"
#include "heightmap.hpp"

#define rgb(r, g, b) ((r << 16) + (g << 8) + b)

const vector FIXED_DIR(0, 0.5f, 0);
const float ANGLE_VARIANCE_DEGREE = 5.0f;
const float SPEED_VARIANCE = 1.0f;

particle_shader _particle_shader;
particle_system _particle(10000);
particle_shooter _redshooter(point(-1.0f, 0, 0), FIXED_DIR, rgb(255, 50, 5), ANGLE_VARIANCE_DEGREE, SPEED_VARIANCE);
particle_shooter _greenshooter(point(0, 0, 0), FIXED_DIR, rgb(25, 255, 25), ANGLE_VARIANCE_DEGREE, SPEED_VARIANCE);
particle_shooter _blueshooter(point(1.0f, 0, 0), FIXED_DIR, rgb(5, 50, 255), ANGLE_VARIANCE_DEGREE, SPEED_VARIANCE);

skybox_shader _skybox_shader;
skybox _skybox;

heightmap_shader _heightmap_shader;
heightmap _heightmap;

GLuint _particle_texid;
GLuint _skybox_texid;
float _runsecond = 0.0f; // the second from start
int _prevx, _prevy;
float _xrotation = 0.0f, _yrotation = 0.0f; // in degrees

glm::mat4 _projection_mat, _view_mat, _skybox_view_mat;

static void
update_view_mat() {
	_view_mat = glm::mat4();
	_view_mat = glm::rotate(_view_mat, glm::radians(-_yrotation), glm::vec3(1.0f, 0, 0));
	_view_mat = glm::rotate(_view_mat, glm::radians(-_xrotation), glm::vec3(0, 1.0f, 0));
	_skybox_view_mat = _view_mat;
   	_view_mat = glm::translate(_view_mat, glm::vec3(0, -1.5f, -5.0f));
}

static glm::mat4
update_mvp_mat(const glm::mat4 &model_mat) {
	return _projection_mat * _view_mat * model_mat;
}

static glm::mat4
update_skybox_mvp_mat(const glm::mat4 &model_mat) {
	return _projection_mat * _skybox_view_mat * model_mat;
}

static GLboolean
init() {
	GLenum err = glewInit();
	if (err != GLEW_OK) {
		logv("fail to init glew");
		return GL_FALSE;
	}

	if (!GLEW_VERSION_3_3) {
		logv("not support for ogl3.3");
		return GL_FALSE;
	}
	logv("support for ogl3.3");

	_particle_texid = texture_util::loadtexture("./res/particle_texture.png");
	if (_particle_texid == 0) {
		logv("fail to load texture file");
		return GL_FALSE;
	}

	std::vector<std::string> files;
	files.push_back("./res/right.png");
	files.push_back("./res/left.png");
	files.push_back("./res/top.png");
	files.push_back("./res/bottom.png");
	files.push_back("./res/back.png");
	files.push_back("./res/front.png");
	_skybox_texid = texture_util::loadcubemap(files);
	if (_skybox_texid == 0) {
		logv("fail to load cubemap file");
		return GL_FALSE;	
	}

	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

	glEnable(GL_DEPTH_TEST);
	glEnable(GL_POINT_SPRITE); // to enable access gl_PointCoord in fragment shader
	glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); // to enable set gl_PointSize in vertex shader

    if (!_particle_shader.init("./res/particle_vertex_shader.glsl", "./res/particle_fragment_shader.glsl")) {
    	logv("fail to init particle shader");
    	return GL_FALSE;
    }

    if (!_skybox_shader.init("./res/skybox_vertex_shader.glsl", "./res/skybox_fragment_shader.glsl")) {
    	logv("fail to init skybox shader");
    	return GL_FALSE;	
    }

    if (!_heightmap.init("./res/heightmap.png")) {
    	logv("fail to init heightmap");
    	return GL_FALSE;
    }

    if (!_heightmap_shader.init("./res/heightmap_vertex_shader.glsl", "./res/heightmap_fragment_shader.glsl")) {
    	logv("fail to init heightmap shader");
    	return GL_FALSE;
    }

    if (!_particle.init()) {
    	logv("fail to init particle system");
    	return GL_FALSE;
    }

    srand((unsigned int)time(NULL));

    logv("init finish successfully");
    return GL_TRUE;
}

static void
draw_skybox() {
	glDepthFunc(GL_LEQUAL);

	glm::mat4 mat = update_skybox_mvp_mat(glm::mat4());
	_skybox_shader.useprogram();
	_skybox_shader.setuniform(mat, _skybox_texid);
	_skybox.binddata();
	_skybox.draw();

	glDepthFunc(GL_LESS);
}

static void
draw_heightmap() {
	glm::mat4 modelmat = glm::scale(glm::mat4(), glm::vec3(100.0f, 10.0f, 100.0f));
	glm::mat4 mat = update_mvp_mat(modelmat);
	_heightmap_shader.useprogram();
	_heightmap_shader.setuniform(mat);
	_heightmap.binddata();
	_heightmap.draw();
}

static void
draw_particle() {
	_redshooter.addparticles(_particle, _runsecond, 5);
   	_greenshooter.addparticles(_particle, _runsecond, 5);
   	_blueshooter.addparticles(_particle, _runsecond, 5);

   	glm::mat4 mat = update_mvp_mat(glm::mat4());

   	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE);
	glDepthMask(GL_FALSE);

   	_particle_shader.useprogram();
   	_particle_shader.setuniform(mat, _runsecond, _particle_texid);
   	_particle.binddata();
   	_particle.draw();

   	glDisable(GL_BLEND);
   	glDepthMask(GL_TRUE);
}

static void
cb_display() {
	// logv("cb_display");
   	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

   	draw_heightmap();
   	draw_skybox();
   	draw_particle();
	
	glutSwapBuffers(); // or we can simply call glFlush
}

static void 
cb_reshape(int width, int height) {
	logv("cb_reshape: width:%d, height:%d", width, height);
	glViewport (0, 0, width, height);

	GLfloat aspect = (GLfloat)width / (GLfloat)height;
	_projection_mat = glm::perspective(glm::radians(45.0f), aspect, 1.0f, 100.0f);
	update_view_mat();
}

static void
cb_close() {
	logv("cb_close");
	_particle_shader.deleteprogram();
	glDeleteTextures(1, &_particle_texid);
	_skybox_shader.deleteprogram();
	glDeleteTextures(1, &_skybox_texid);
}

static void
cb_timer(int param) {
	_runsecond += 0.017f;
	glutPostRedisplay();
	glutTimerFunc(17, cb_timer, 0);
}

static void 
cb_mouse(int button, int state, int x, int y) {
	// logv("cb_mouse, button:%d state:%d x:%d y:%d", button, state, x, y);
	if (state == GLUT_DOWN) {
		_prevx = x;
		_prevy = y;
	}
}

static void 
handledrag(float deltax, float deltay) {
	_xrotation += deltax / 16.0f;
	_yrotation += deltay / 16.0f;
	_yrotation = _yrotation < -90.0f ? -90.0f : _yrotation;
	_yrotation = _yrotation > 90.0f ? 90.0f : _yrotation;

	update_view_mat();
}

static void 
cb_motion(int x, int y) {
	// logv("motion, x:%d y:%d", x, y);
	float deltax = x - _prevx;
	float deltay = y - _prevy;
	_prevx = x;
	_prevy = y;

	handledrag(deltax, deltay);
}

int
main(int argc, char *argv[]) {
	glutInit(&argc, argv);
	glutInitWindowSize(800, 600);
	glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
	glutInitWindowPosition(0, 0);
	
	glutCreateWindow("AirHockey - heightmap");
	
	if (init() == GL_FALSE)
		return 0;

	glutDisplayFunc(cb_display);
	glutReshapeFunc(cb_reshape);
	glutCloseFunc(cb_close);
	glutTimerFunc(17, cb_timer, 0); // 60 fps
	glutMouseFunc(cb_mouse);
	glutMotionFunc(cb_motion);

	glutMainLoop();
	return 0;
}
